﻿using System;
using biz.ritter.javapi.lang;
using biz.ritter.javapi.lang.annotation;
using biz.ritter.javapi.lang.reflect;
using biz.ritter.javapi.util;
using Nofs.Net.Annotations;
using Nofs.Net.Common.Interfaces.Library;
using Nofs.Net.nofs.metadata.interfaces;

namespace Nofs.Net.AnnotationDriver
{

    //@SuppressWarnings("unchecked")
    public class AttributeAccessor : IAttributeAccessor
    {
        private AttributeCache _isDomainObject;
        private AttributeCache _isFolderObject;
        private AttributeCache _isRootObject;
        private ClassMethodCache _nameGetter;
        private ClassMethodCache _containerSetter;
        private ClassMethodCache _containerManagerSetter;
        private INoFSClassLoader _classLoader;
        private Map<Class, bool> _isAbstractAndHasAFolderSubclassMap;

        public AttributeAccessor()
            : this(new NoFSClassLoader(ClassLoader.getSystemClassLoader()))
        {

        }

        public AttributeAccessor(INoFSClassLoader classLoader)
        {
            _isRootObject = new AttributeCache(typeof(IRootFolderObject).getClass());
            _isDomainObject = new AttributeCache(typeof(IDomainObject).getClass());
            _nameGetter = new ClassMethodCache(typeof(IProvidesName).getClass());
            _containerSetter = new ClassMethodCache(typeof(INeedsContainer).getClass());
            _containerManagerSetter = new ClassMethodCache(typeof(INeedsContainerManager).getClass());
            _isFolderObject = new AttributeCache(typeof(IFolderObject).getClass(), typeof(IDomainObject).getClass());
            _classLoader = classLoader;
            _isAbstractAndHasAFolderSubclassMap = new HashMap<Class, bool>();
        }

        public Class FindObjectTypeToCreateForMknod(Object obj, Method method) //throws Exception 
        {
            if (obj is Class || obj is Type)
            {
                throw new System.Exception("Cannot accept class or type");
            }
            return FindObjectTypeToCreateForMknod(obj, GetInnerCollectionType(method));
        }

        public Class FindObjectTypeToCreateForMkDir(Object obj, Method method) //throws Exception 
        {
            if (obj is Class || obj is Type)
            {
                throw new System.Exception("Cannot accept class or type");
            }
            return FindObjectTypeToCreateForMkDir(obj, GetInnerCollectionType(method));
        }

        public Class FindObjectTypeToCreateForMknod(Object obj) //throws Exception 
        {
            if (obj is Class || obj is Type)
            {
                throw new System.Exception("Cannot accept class or type");
            }
            return FindObjectTypeToCreateForMknod(obj, GetInnerCollectionType(obj));
        }

        private Class FindObjectTypeToCreateForMknod(Object obj, Class innerCollectionType) //throws Exception 
        {
            ArrayList<Class> matches = new ArrayList<Class>();
            if (!isFolderObject(innerCollectionType, true) && isDomainObject(innerCollectionType) && !Modifier.isAbstract(innerCollectionType.getModifiers()))
            {
                matches.add(innerCollectionType);
            }
            else if (innerCollectionType.GetType().IsAbstract)
            {
                foreach (Class folderType in _classLoader.LoadClassesWithAnnotation(MarkerTypes.DomainObject))
                {
                    if (innerCollectionType.isAssignableFrom(folderType) && !isFolderObject(folderType) && !Modifier.isAbstract(folderType.getModifiers()))
                    {
                        matches.add(folderType);
                    }
                }
            }
            return FilterOutResults(obj, matches, innerCollectionType);
        }

        private Class FilterOutResults(Object obj, ArrayList<Class> matches, Class innerCollectionType) //throws Exception 
        {
            if (matches.size() > 1)
            {
                IFolderObjectProperties folderProperties = GetFolderObject(obj);
                if (folderProperties.ChildTypeFilterMethod().length() > 0)
                {
                    ArrayList<Class> filteredTypes = new ArrayList<Class>();
                    foreach (Class type in matches)
                    {
                        if (InvokeFilterMethod(folderProperties, obj, type))
                        {
                            filteredTypes.add(type);
                        }
                    }
                    matches = filteredTypes;
                }
            }

            if (matches.size() == 1)
            {
                return matches.get(0);
            }
            else if (matches.size() > 1)
            {
                StringBuffer foundTypes = new StringBuffer();
                foundTypes.append("Found more than one possible type for mknod: ");
                foreach (Class found in matches)
                {
                    foundTypes.append(found.getSimpleName());
                    foundTypes.append(",");
                }
                throw new System.Exception(foundTypes.toString());
            }
            throw new System.Exception("Cannot find a type to use for creation. Inner Type = " + innerCollectionType.getSimpleName());
        }

        private bool InvokeFilterMethod(IFolderObjectProperties properties, Object obj, Class typeToCheck) //throws Exception 
        {
            foreach (Method method in obj.getClass().getDeclaredMethods())
            {
                if (method.getName().compareTo(properties.ChildTypeFilterMethod()) == 0 &&
                    method.getReturnType() == typeof(bool).getClass() &&
                    method.getParameterTypes().Length == 1 &&
                    method.getParameterTypes()[0] == typeof(Class).getClass())
                {
                    return (bool)method.invoke(obj, typeToCheck);
                }
            }
            throw new System.Exception("could not find method with name = " + properties.ChildTypeFilterMethod() + " on object of type " + obj.getClass().getSimpleName());
        }

        public Class FindObjectTypeToCreateForMkDir(Object obj) //throws Exception 
        {
            if (obj is Class || obj is Type)
            {
                throw new System.Exception("Cannot accept class or type");
            }
            return FindObjectTypeToCreateForMkDir(obj, GetInnerCollectionType(obj));
        }

        private Class FindObjectTypeToCreateForMkDir(Object obj, Class innerCollectionType) //throws Exception 
        {
            ArrayList<Class> matches = new ArrayList<Class>();
            if (isFolderObject(innerCollectionType, true) 
                && !Modifier.isAbstract(innerCollectionType.getModifiers()))
            {
                matches.add(innerCollectionType);
            }
            else if (Modifier.isAbstract(innerCollectionType.getModifiers()))
            {
                foreach (Class folderType in _classLoader.LoadClassesWithAnnotation(MarkerTypes.FolderObject))
                {
                    if (innerCollectionType.isAssignableFrom(folderType) && !Modifier.isAbstract(folderType.getModifiers()))
                    {
                        matches.add(folderType);
                    }
                }
            }
            return FilterOutResults(obj, matches, innerCollectionType);
        }

        public Class GetInnerCollectionType(Method method) //throws Exception 
        {
            if (method.getReturnType() == typeof(ParameterizedType).getClass())
            {
                return GetInnerType(new ParameterizedType(method.getReturnType()));
            }
            throw new System.Exception("cannot find inner type");
        }

        public Class GetInnerCollectionType(Object sender) //throws Exception 
        {
            if (sender is Type)
            {
                throw new System.Exception("argument of wrong type");
            }

            Class cls = sender.getClass();
            Type objType = sender.GetType();

            return GetInnerCollectionType(objType, cls);
        }

        private static Class GetInnerCollectionType(Type objType, Class cls) //throws Exception 
        {
            if (!(objType == typeof(ParameterizedType)) 
                && cls.getSuperclass() == typeof(ParameterizedType).getClass()
                && typeof(List).getClass().isAssignableFrom(cls.getSuperclass()))
            {
                objType = cls.getSuperclass().GetType() ;
            }

            if (objType == typeof(ParameterizedType))
            {
                return GetInnerType(new ParameterizedType(objType));
            }
            else
            {
                if (typeof(List).getClass().isAssignableFrom(cls))
                {
                    foreach (Type typ in cls.getGenericInterfaces())
                    {
                        if ((typ == typeof(ParameterizedType)))
                        {
                            ParameterizedType paraType = new ParameterizedType(typ);
                            if ((paraType.getRawType() == typeof(Class)) &&
                                typeof(List).getClass().isAssignableFrom(paraType.getRawType().getClass()))
                            {
                                return GetInnerType(new ParameterizedType(typ));
                            }
                        }
                    }
                    if (cls.getGenericSuperclass() != null && cls.getGenericSuperclass() != typeof(Object))
                    {
                        return GetInnerCollectionType(cls.getGenericSuperclass(), cls.getSuperclass());
                    }
                    throw new System.Exception("inner type cannot be found!");
                }
                return typeof(Object).getClass();
            }
        }

        private static Class GetInnerType(ParameterizedType paraType) //throws Exception 
        {
            foreach (Type typeArgument in paraType.getActualTypeArguments())
            {
                if (typeArgument == typeof(Class))
                {
                    foreach (IAnnotation annotation in typeArgument.getClass().getAnnotations())
                    {
                        if (annotation.annotationType() == typeof(IDomainObject))
                        {
                            return typeArgument.getClass();
                        }
                    }
                }
            }
            String error = "cannot find inner type";
            if (paraType.getActualTypeArguments().Length == 1 &&
                    paraType.getActualTypeArguments()[0] == typeof(Class))
            {
                Class objType = paraType.getActualTypeArguments()[0].getClass();
                error += ", " + objType.getSimpleName() + " is not a domain object!";
            }
            throw new System.Exception(error);
        }

        public bool isExecutable(Method method)
        {
            foreach (IAnnotation annotation in method.getAnnotations())
            {
                if (annotation.annotationType() == typeof(IExecutable))
                {
                    return true;
                }
            }
            return false;
        }

        public IDomainObjectProperties GetDomainObject(Method method) //throws Exception 
        {
            if (!isDomainObject(method))
            {
                throw new System.Exception("not a domain object");
            }
            IDomainObject dObj = null;
            foreach (IAnnotation annotation in method.getAnnotations())
            {
                if (annotation.annotationType() == typeof(IDomainObject))
                {
                    dObj = (IDomainObject)annotation;
                    break;
                }
            }
            if (dObj == null)
            {
                return new DomainObjectFacade(new DummyDomainObject());
            }
            return new DomainObjectFacade(dObj);
        }

        public IFolderObjectProperties GetFolderObject(Method method) //throws Exception 
        {
            if (!isFolderObject(method))
            {
                throw new System.Exception("not a folder object");
            }
            IFolderObject fObj = null;
            foreach (IAnnotation annotation in method.getAnnotations())
            {
                if (annotation.annotationType() == typeof(IFolderObject))
                {
                    fObj = (IFolderObject)annotation;
                    break;
                }
            }
            if (fObj == null)
            {
                return new FolderObjectFacade(new DummyFolderObject());
            }
            return new FolderObjectFacade(fObj);
        }

        private class DummyFolderObject : IFolderObject
        {
            //@Override
            public bool CanAdd()
            {
                return true;
            }

            //@Override
            public String CanAddMethod()
            {
                return "";
            }

            //@Override
            public bool CanRemove()
            {
                return true;
            }

            //@Override
            public String CanRemoveMethod()
            {
                return "";
            }

            //@Override
            public Class annotationType()
            {
                return typeof(IFolderObject).getClass();
            }

            //@Override
            public String ChildTypeFilterMethod()
            {
                return "";
            }

        }

        private class DummyDomainObject : IDomainObject
        {
            //@Override
            public bool CanWrite()
            {
                return true;
            }

            //@Override
            public Class annotationType()
            {
                return typeof(IDomainObject).getClass();
            }

            //@Override
            public Class LayoutObject()
            {
                return typeof(Object).getClass();
            }

        }

        private class DomainObjectFacade : IDomainObjectProperties
        {
            private IDomainObject _domainObject;
            public DomainObjectFacade(IDomainObject obj)
            {
                _domainObject = obj;
            }
            //@Override
            public bool CanWrite()
            {
                return _domainObject.CanWrite();
            }
        }

        private class FolderObjectFacade : IFolderObjectProperties
        {
            private IFolderObject _folder;

            public FolderObjectFacade(IFolderObject folder)
            {
                _folder = folder;
            }

            //@Override
            public bool CanAdd()
            {
                return _folder.CanAdd();
            }

            //@Override
            public String CanAddMethod()
            {
                return _folder.CanAddMethod();
            }

            //@Override
            public bool CanRemove()
            {
                return _folder.CanRemove();
            }

            //@Override
            public String CanRemoveMethod()
            {
                return _folder.CanRemoveMethod();
            }

            public String ChildTypeFilterMethod()
            {
                return _folder.ChildTypeFilterMethod();
            }
        }

        public IFolderObjectProperties GetFolderObject(Object obj) //throws Exception 
        {
            if (!isFolderObject(obj))
            {
                throw new System.Exception("not a folder object");
            }
            IFolderObject fObj = null;
            foreach (IAnnotation annotation in obj.getClass().getAnnotations())
            {
                if (annotation.annotationType() == typeof(IFolderObject))
                {
                    fObj = (IFolderObject)annotation;
                    break;
                }
            }
            if (fObj == null)
            {
                return new FolderObjectFacade(new DummyFolderObject());
            }
            return new FolderObjectFacade(fObj);
        }

        public IDomainObjectProperties GetDomainObject(Object sender) //throws Exception 
        {
            if (!isDomainObject(sender))
            {
                throw new System.Exception("not a domain object");
            }
            IDomainObject dObj = null;

            foreach (IAnnotation annotation in sender.getClass().getAnnotations())
            {
                if (annotation.annotationType() == typeof(IDomainObject))
                {
                    dObj = (IDomainObject)annotation;
                    break;
                }
            }
            if (dObj == null)
            {
                return new DomainObjectFacade(new DummyDomainObject());
            }
            return new DomainObjectFacade(dObj);
        }

        public bool isFSRoot(Method method)
        {
            return _isRootObject.hasAnnotation(method);
        }

        public bool isFSRoot(Object sender) //throws Exception 
        {
            return _isRootObject.hasAnnotation(sender);
        }

        public bool isFolderObject(Method method) //throws Exception
        {
            bool isExactlyAFolder = _isFolderObject.hasAnnotation(method);
            return isExactlyAFolder || isAbstractAndHasAFolderSubclass(method.getReturnType());
        }

        //TODO: synchronized this function
        private bool isAbstractAndHasAFolderSubclass(Class objType) //throws Exception 
        {
            if (!_isAbstractAndHasAFolderSubclassMap.containsKey(objType))
            {
                bool result = false;
                if (Modifier.isAbstract(objType.getModifiers()))
                {
                    foreach (Class folderType in _classLoader.LoadClassesWithAnnotation(MarkerTypes.FolderObject))
                    {
                        if (objType.isAssignableFrom(folderType))
                        {
                            result = true;
                            break;
                        }
                    }
                }
                _isAbstractAndHasAFolderSubclassMap.put(objType, result);
            }
            return _isAbstractAndHasAFolderSubclassMap.get(objType);
        }

        public bool isDomainObject(Method method)
        {
            return _isDomainObject.hasAnnotation(method);
        }

        public bool isFolderObject(Class type, bool strict) //throws Exception 
        {
            return _isFolderObject.hasAnnotation(type) || (!strict && isAbstractAndHasAFolderSubclass(type));
        }

        public bool isFolderObject(Object obj, bool strict) //throws Exception
        {
            return _isFolderObject.hasAnnotation(obj) || (!strict && isAbstractAndHasAFolderSubclass(obj.getClass()));
        }

        public bool isFolderObject(Class type) //throws Exception
        {
            return isFolderObject(type, false);
        }

        public bool isFolderObject(Object obj) //throws Exception 
        {
            return isFolderObject(obj, false);
        }

        public bool isDomainObject(Object obj) //throws Exception 
        {
            return _isDomainObject.hasAnnotation(obj);
        }

        public bool isDomainObject(Class cls)
        {
            return _isDomainObject.hasAnnotation(cls);
        }

        public void setContainerIfAttributeExists(Object obj, IDomainObjectContainer container) //throws IllegalArgumentException, IllegalAccessException, InvocationTargetException 
        {
            if (isDomainObject(obj.getClass()))
            {
                Method setter = _containerSetter.getCachedMethod(obj.getClass());
                if (setter != null)
                {
                    setter.invoke(obj, container);
                }
            }
        }

        public void setContainerManagerIfAttributeExists(Object obj, IDomainObjectContainerManager manager) //throws IllegalArgumentException, IllegalAccessException, InvocationTargetException 
        {
            if (isDomainObject(obj.getClass()))
            {
                Method setter = _containerManagerSetter.getCachedMethod(obj.getClass());
                if (setter != null)
                {
                    setter.invoke(obj, manager);
                }
            }
        }

        public String getNameFromObject(Object obj) //throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, Exception 
        {
            if (obj is Method)
            {
                Method method = (Method)obj;
                String name = method.getName();
                if (name.startsWith("get"))
                {
                    name = name.substring(3);
                }
                return name;
            }
            else
            {
                String name = obj.getClass().getSimpleName() + "_" + obj.hashcode().ToString();
                if (isDomainObject(obj.getClass()))
                {
                    Method getter = null;
                    foreach (Method possibleGetter in _nameGetter.getCachedMethods(obj.getClass()))
                    {
                        if (possibleGetter.getReturnType() == typeof(String).getClass()
                            && possibleGetter.getParameterTypes().Length == 0)
                        {
                            getter = possibleGetter;
                            break;
                        }
                    }
                    if (getter != null)
                    {
                        Object nameObj = getter.invoke(obj, (Object[])null);
                        if (nameObj != null)
                        {
                            name = nameObj.toString();
                        }
                    }
                }
                return name;
            }
        }

        public bool canSetNameForObject(Object obj) //throws Exception 
        {
            if (!(obj is Method) && isDomainObject(obj.getClass()))
            {
                Method setter = null;
                foreach (Method possibleSetter in _nameGetter.getCachedMethods(obj.getClass()))
                {
                    if (possibleSetter.getReturnType() == typeof(void).getClass() &&
                       possibleSetter.getParameterTypes().Length == 1 &&
                       possibleSetter.getParameterTypes()[0] == typeof(String).getClass())
                    {
                        setter = possibleSetter;
                        break;
                    }
                }
                if (setter != null)
                {
                    return true;
                }
            }
            return false;
        }

        public void setNameForObject(Object obj, String name) //throws IllegalArgumentException, IllegalAccessException, InvocationTargetException 
        {
            if (!(obj is Method) && isDomainObject(obj.getClass()))
            {
                Method setter = null;

                foreach (Method possibleSetter in _nameGetter.getCachedMethods(obj.getClass()))
                {
                    if (possibleSetter.getReturnType() == typeof(void).getClass() &&
                       possibleSetter.getParameterTypes().Length == 1 &&
                       possibleSetter.getParameterTypes()[0] == typeof(String).getClass())
                    {
                        setter = possibleSetter;
                        break;
                    }
                }
                if (setter != null)
                {
                    setter.invoke(obj, new Object[] { name });
                }
            }
        }

        public IGetterSetterPair GetLastAccessTimePair(Object obj) //throws Exception 
        {
            return new GetterSetterPair(obj, typeof(Date).getClass(), typeof(IProvidesLastAccessTime).getClass());
        }

        public IGetterSetterPair GetLastModifiedTimePair(Object obj) //throws Exception
        {
            return new GetterSetterPair(obj, typeof(Date).getClass(), typeof(IProvidesLastModifiedTime).getClass());
        }

        public IGetterSetterPair GetCreateTimePair(Object obj) //throws Exception 
        {
            return new GetterSetterPair(obj, typeof(Date).getClass(), typeof(IProvidesCreateTime).getClass());
        }

        public IGetterSetterPair GetModePair(Object obj) //throws Exception 
        {
            return new GetterSetterPair(obj, typeof(int).getClass(), typeof(IProvidesMode).getClass());
        }

        public IGetterSetterPair GetUIDPair(Object obj) //throws Exception
        {
            return new GetterSetterPair(obj, typeof(int).getClass(), typeof(IProvidesUID).getClass());
        }

        public IGetterSetterPair GetGIDPair(Object obj) //throws Exception
        {
            return new GetterSetterPair(obj, typeof(int).getClass(), typeof(IProvidesGID).getClass());
        }

        public bool HasAnyCustomMetadataManagement(Object obj) //throws Exception 
        {
            foreach (Method method in obj.getClass().getDeclaredMethods())
            {
                foreach (IAnnotation annotation in method.getAnnotations())
                {
                    if (annotation.annotationType() == typeof(IProvidesLastAccessTime) ||
                       annotation.annotationType() == typeof(IProvidesLastModifiedTime) ||
                       annotation.annotationType() == typeof(IProvidesCreateTime) ||
                       annotation.annotationType() == typeof(IProvidesMode) ||
                       annotation.annotationType() == typeof(IProvidesUID) ||
                       annotation.annotationType() == typeof(IProvidesGID))
                    {
                        return true;
                    }
                }
            }
            return false;
        }
    }

}
